package primitives;

import light.Light;
import colour.Colour;
import ray.Ray;
import surface.Surface;
import vector.Vector;

/**
 * Contain the element for defining a Sphere. Provide the method for operation
 * on them.
 * 
 * @author Yuting Wu
 * 
 */

public class Sphere extends AbstractShape {
	// the postion of the center of the sphere
	private Vector pCenter;
	// the radius of the sphere
	private double radius;
	// the northpole of the sphere used for picture project
	private Vector vNorthPole;
	// the equator of the sphere used for picture project
	private Vector vEquator;

	// the vector of the hitpoint
	private Vector vHitPoint;
	// the position of the hitpoint
	private Vector pHitPoint;
	// the distance from the origin to the hitpoint
	private double disHitPoint = (double) -1;
	// check whether the hitpoint is inside the sphere
	private boolean insideOrOutside = false;

	private double disOriToCloPoint = (double) -1;
	private double disCPSubRadius = (double) -1;

	/**
	 * 
	 * @param center
	 * @param radius
	 * @param vNorthPole
	 * @param vEquator
	 * @param surface
	 */

	public Sphere(Vector center, double radius, Vector vNorthPole,
			Vector vEquator, Surface surface) {
		super(surface);
		this.pCenter = center;
		this.radius = radius;

		if (vNorthPole == null) {
			this.vNorthPole = new Vector(0, 0, 1);
		} else {
			this.vNorthPole = vNorthPole;
		}
		if (vEquator == null) {
			this.vEquator = new Vector(0, 1, 0);
		} else {
			this.vEquator = vEquator;
		}

	}

	/**
	 * @return the center
	 */
	public Vector getCenter() {
		return pCenter;
	}

	/**
	 * @return the radius
	 */
	public double getRadius() {
		return radius;
	}

	/**
	 * @return the vNorthPole
	 */
	public Vector getNorthPole() {
		return vNorthPole;
	}

	/**
	 * @return the vEquator
	 */
	public Vector getEquator() {
		return vEquator;
	}

	/**
	 * @return the surface
	 */
	public Surface getSurface() {
		return surface;
	}

	/**
	 * Get the colour of the hitpoint on the sureface of the sphere
	 */
	public Colour getColorAt(Vector pHitPoint, Vector vHitPoint) {
		Colour result;
		if (surface == null) {
			result = new Colour(0, 0, 0);
			System.out.println("no surface");
		} else {
			if (surface.getType() == 1) {
				result = this.surface.getColour();
			} else if (surface.getType() == 0) {
				result = new Colour(0, 0, 0);
			} else {
				// Calculate the basic UV for locate the bitmap
				double uTransform;
				double vTransform;
				double angleRS;
				Vector vNorthPole = this.getNorthPole();
				Vector vEquator = this.getEquator();
				final double angledot = vHitPoint.multiply((double) -1)
						.dotProduct(vNorthPole);
				angleRS = Math.acos(angledot);
				vTransform = angleRS / Math.PI;

				if (vTransform == 1 || vTransform == 0) {
					uTransform = 0;
				} else {
					double temp = vEquator.dotProduct(vHitPoint)
							/ Math.sin(angleRS);
					final double theta = Math.acos(temp) / (2 * Math.PI);
					temp = vEquator.crossProduct(vNorthPole).dotProduct(
							vHitPoint);
					if (temp > 0) {
						uTransform = theta;
					} else {
						uTransform = 1 - theta;
					}
				}

				result = surface.getColourIn(uTransform, vTransform);
			}
		}
		return result;
	}

	@Override
	/**
	 * get the diffuse value from the sphere
	 */
	public double getDiffuse() {
		// set the default
		double diffuse;
		if (surface != null) {
			diffuse = this.surface.getDiffuse();
		} else {
			diffuse = 1.0;
		}

		return diffuse;
	}

	@Override
	/**
	 * get the reflect value from the sphere
	 */
	public double getReflect() {
		double reflect;
		if (surface != null) {
			reflect = this.surface.getReflect();
		} else {
			reflect = 0.0;
		}

		return reflect;

	}

	@Override
	/**
	 * Input a ray, to judge whether it is hit and calculate the position and
	 * vector of the hitpoint To judge whether the ray hit the sphere It is not
	 * hit when the tca<0 and t2hc<0
	 * 
	 * @param ray
	 *            ,simpleOrComplex
	 * @return boolean to show if success
	 * 
	 */
	public boolean calMutiIntersection(Ray raylist, Boolean ifSimple) {
		double disRayToCenter = this.calDisRayToCenter(raylist);
		boolean ifHit = false;
		// inside
		if (insideOrOutside) {
			ifHit = true;
		} else {
			if (disOriToCloPoint >= 0) {

				final double radius2 = radius * radius;
				final double ca2 = disOriToCloPoint * disOriToCloPoint;
				disCPSubRadius = radius2 - disRayToCenter + ca2;

				if (disCPSubRadius >= 0) {
					ifHit = true;
				}
			}
		}
		return ifHit;
	}

	/**
	 * The square of the distance between the point on the ray closest to the
	 * center of the sphere and substract the distance from the square o fthe
	 * radius of the sphere.Whenever smaller than 0 no hit
	 * 
	 * @param rayInput
	 */
	private double calDisRayToCenter(final Ray rayInput) {

		final Vector vCayToCenter = pCenter.subtract(rayInput.getOrigin());
		final double disRayToCenter = vCayToCenter.dotProduct(vCayToCenter);
		if (disRayToCenter > radius * radius) {
			insideOrOutside = false;
		} else {

			insideOrOutside = true;
		}
		disOriToCloPoint = vCayToCenter.dotProduct(rayInput.getDesiredRay());
		return disRayToCenter;

	}

	@Override
	/**
	 * Calculate the intersection distance
	 * 
	 * @return disHitPoint
	 */
	public double getDisHitPointIndex() {
		if (insideOrOutside) {
			// rays inside the sphere
			disHitPoint = disOriToCloPoint
					+ Math.sqrt(Math.abs(disCPSubRadius));

		} else {
			// rays outside the sphere
			disHitPoint = disOriToCloPoint - Math.sqrt(disCPSubRadius);
		}
		return disHitPoint;
	}

	@Override
	/**
	 * return the position of the hitpoint
	 * 
	 * @return pHitPoint
	 */
	public Vector getPositionHitPointIndex() {
		return pHitPoint;
	}

	@Override
	/**
	 * return the vector of the hitpoint
	 * @return vHitPoint
	 */
	public Vector getVectorHitPointIndex() {
		return vHitPoint;
	}

	@Override
	/**
	 * check whether the origin of the ray is inside or outside of the sphere
	 * 
	 * @return the insideOrOutside
	 */
	public boolean isInsideOrOutside() {
		return insideOrOutside;
	}

	/**
	 * calculate position of the hitpoint
	 * 
	 * @param rayInput
	 * @param distance
	 * @return
	 */
	private Vector calIntersection(final Ray rayInput, final double distance) {

		Vector pHitPoint;
		pHitPoint = rayInput.getOrigin().add(
				rayInput.getDesiredRay().multiply(distance));
		return pHitPoint;
	}

	/**
	 * calculate the vector for the hitpoint
	 * 
	 * @param pHitPoint
	 * @return
	 */
	private Vector calVectorIntersectionPoint(final Vector pHitPoint) {
		Vector vHitPoint;
		if (insideOrOutside) {
			// rays inside the sphere
			vHitPoint = pHitPoint.subtract(pCenter).multiply((double) -1)
					.divide(radius).normalise();

		} else {
			// rays outside the sphere
			vHitPoint = pHitPoint.subtract(pCenter).divide(radius).normalise();
		}
		return vHitPoint;
	}

	@Override
	/**
	 * do the calculation of position and vector for hitpoint and distance from
	 * the origin to the hitpoint
	 * 
	 * @param rayInput
	 */
	public void runFurtherCalculation(Ray rayInput) {
		// //inside
		// disHitPoint = calDisIntersection();
		if (insideOrOutside) {
			pHitPoint = this.calIntersection(rayInput, disHitPoint);
			vHitPoint = this.calVectorIntersectionPoint(pHitPoint);
		} else {
			if (disOriToCloPoint >= 0 && disCPSubRadius >= 0) {
				pHitPoint = this.calIntersection(rayInput, disHitPoint);
				vHitPoint = this.calVectorIntersectionPoint(pHitPoint);
			}
		}

	}

	@Override
	/**
	 * calculate the intensity of the light through the light and normal of the hitpoint
	 * the sphere has the special case that the camera might inside the sphere
	 */
	public double lightIntensityCalculation(Light singleLight,Vector pHitPoint,Vector vHitPoint) {
		
		// offset the slightly in the direction away from the surface

		Vector s0 = pHitPoint.add(vHitPoint.multiply(0.0001));
		Vector sd = singleLight.getLight().subtract(s0).normalise();

		Vector vCenterToLight = pCenter.subtract(singleLight.getLight());
		double disCenterToLight = vCenterToLight.dotProduct(vCenterToLight);

		// the sphere has the situation that the camera insides the sphere
		if (insideOrOutside) {

			// light outside
			if (disCenterToLight > radius * radius) {
				return 0.0;
			} else {
				return 1.0;
			}
		// the sphere has the situation that the camera outsides the sphere
		} else {

			// light outside
			if (disCenterToLight > radius * radius) {
				// get the diffuse of the object multiply with the angle
				return Math.abs(vHitPoint.dotProduct(sd))*this.getDiffuse();
			} else {
				return 0.0;
			}

		}
	}

	

}
